import abc
import traceback
import weakref
from threading import Lock, Thread, current_thread
from time import sleep, time
from typing import Any, Callable, Optional


class LiveTimer(abc.ABC):
    def __init__(self, tick_func: Callable, *tick_args: Any, interval: Optional[float] = 1 / 30):
        self._tick_func = tick_func
        self._tick_args = tick_args
        self._interval = interval or 0

        self._lock = Lock()
        self._thread = None

    @property
    def interval(self) -> float:
        return self._interval

    @property
    def alive(self) -> bool:
        with self._lock:
            return self._thread is not None

    def start(self):
        with self._lock:
            if self._thread:
                return
            self._thread = Thread(
                target=self._thread_func,
                args=(weakref.ref(self), self._interval),
                daemon=True)
            self._thread.start()

    def stop(self):
        with self._lock:
            if not self._thread:
                return
            thread = self._thread
            self._thread = None
        if thread is not current_thread():
            thread.join()

    def join(self):
        thread = self._thread
        if thread:
            thread.join()

    @staticmethod
    def _thread_func(self_ref, interval):
        scheduled_time = time() + interval
        while True:
            # check if alive
            self = self_ref()
            if self is None:
                break  # thread stops if the LiveTimer instance is destructed.
            with self._lock:
                if self._thread is not current_thread():
                    break  # thread stops if the _thread is not current thread.

            # tick
            try:
                self._tick_func(*self._tick_args)
            except Exception:
                traceback.print_exc()
                self.stop()

            # !! important !! release reference to LiveTime instance before sleeping
            del self

            # sleep for a certain time
            now_time = time()
            sleep_time = scheduled_time - now_time
            if sleep_time > 0:
                sleep(sleep_time)
                scheduled_time = scheduled_time + interval
            else:
                scheduled_time = now_time + interval
